#!/usr/bin/env python3
# Copyright 2019 The Emscripten Authors.  All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License.  Both these licenses can be
# found in the LICENSE file.

"""Update 'symbols' files based on the contents of libraries in the cache.

The symbols files looks like the output of `nm` but only contain external
symbols, ignores undefined symbols (we just care about what is provided, not
what is required), and the symbols from all object in that archive are sorted
and de-duplicated.
"""

import sys
import argparse
import os
import filecmp

root_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, root_dir)
symbols_base_dir = os.path.join(root_dir, 'system', 'lib', 'symbols')
asmjs_symbols_dir = os.path.join(symbols_base_dir, 'asmjs')
wasm_symbols_dir = os.path.join(symbols_base_dir, 'wasm')

from tools import shared, cache
from tools.system_libs import Library

# Libraries that need .symbols file
target_libs = ['libal', 'libgl', 'libhtml5']


def get_symbols_dir():
  if shared.Settings.WASM_BACKEND:
    return wasm_symbols_dir
  else:
    return asmjs_symbols_dir


def is_symbol_file_supported(symbol_file):
  if shared.Settings.WASM_BACKEND:
    return os.path.abspath(symbol_file).startswith(wasm_symbols_dir)
  else:
    return os.path.abspath(symbol_file).startswith(asmjs_symbols_dir)


# Given a symbol file name, returns a matching library file name.
def get_lib_file(symbol_file):
  basename = os.path.splitext(os.path.basename(symbol_file))[0]
  cache_dir = cache.Cache().dirname
  lib_extension = 'a' if shared.Settings.WASM_BACKEND else 'bc'
  return os.path.join(cache_dir, basename + '.' + lib_extension)


# Given a library file name, returns a matching symbols file name.
def get_symbol_file(lib_file):
  basename = os.path.splitext(os.path.basename(lib_file))[0]
  return os.path.join(get_symbols_dir(), basename + '.symbols')


def filter_and_sort(symbols):
  lines = symbols.splitlines()
  lines = [l.rstrip() for l in lines]
  lines = [l for l in lines if l and l[-1] != ':']

  # Extract symbol type and name
  symbols = [l.split()[-2:] for l in lines]

  # Remove local symbols (lowercase type name)
  symbols = [(typ, name) for typ, name in symbols if typ.isupper()]

  symbol_map = {}
  for sym_type, sym_name in symbols:
    assert sym_type in ('W', 'T', 'D', 'C')
    existing_type = symbol_map.get(sym_name)
    if not existing_type:
      symbol_map[sym_name] = sym_type
      continue
    elif existing_type == 'W' and sym_type != 'W':
      symbol_map[sym_name] = sym_type
    elif sym_type != 'W':
      # We don't expect to see two defined version of a given symbol
      if existing_type != sym_type:
        print('Unexpected symbol types found: %s: %s vs %s' %
              (sym_name, existing_type, sym_type))
  symbols = [(typ, name) for name, typ in symbol_map.items()]

  # sort by name
  symbols.sort(key=lambda s: s[1])

  lines = ['# Auto-generated by tools/update_symbols.py.  DO NOT EDIT.']
  for typ, name in symbols:
    lines.append("-------- %s %s" % (typ, name))

  return '\n'.join(lines) + '\n'


def generate_symbol_file(symbol_file, lib_file):
  """Regenerate the contents of a given symbol file."""
  output = shared.run_process([shared.LLVM_NM, '-g', lib_file, '-defined-only'],
                              stdout=shared.PIPE).stdout
  new_symbols = filter_and_sort(output)
  with open(symbol_file, 'w') as f:
    f.write(new_symbols)


def main():
  parser = argparse.ArgumentParser(
      description=__doc__, usage="%(prog)s [options] [files ...]")
  parser.add_argument('files', metavar='files', type=str, nargs='*',
                      help='symbol files to regenerate (default: all)')
  args = parser.parse_args()

  if not shared.Settings.WASM:
    sys.stderr.write('This script only runs in WASM mode\n')
    sys.exit(1)

  shared.safe_ensure_dirs(get_symbols_dir())
  if args.files:
    for symbol_file in args.files:
      if not is_symbol_file_supported(symbol_file):
        print('skipping %s because it is not supported' % symbol_file)
        continue
      lib_file = get_lib_file(symbol_file)
      if not os.path.exists(lib_file):
        print('skipping %s because %s does not exist' % (symbol_file, lib_file))
        continue
      generate_symbol_file(symbol_file, lib_file)
  else:
    # Build all combinations of libraries and generate symbols files
    system_libs = Library.get_all_variations()
    for lib in system_libs.values():
      if lib.name not in target_libs:
        continue
      lib_file = lib.get_path()
      symbol_file = get_symbol_file(lib_file)
      generate_symbol_file(symbol_file, lib_file)

    # Not to generate too many symbols files with the same contents, if there
    # exists a default symbols file (that has a library name without any
    # suffices, such as -mt) and its contents are the same as another symbols
    # file with suffices, remove it.
    for lib in system_libs.values():
      if lib.name not in target_libs:
        continue
      lib_file = lib.get_path()
      symbol_file = get_symbol_file(lib_file)
      default_symbol_file = os.path.join(get_symbols_dir(),
                                         lib.name + '.symbols')
      if symbol_file != default_symbol_file and \
         os.path.isfile(default_symbol_file) and \
         filecmp.cmp(default_symbol_file, symbol_file):
        os.unlink(symbol_file)
  return 0


if __name__ == '__main__':
  sys.exit(main())
